Skip to content

Check the subtracted type when a subtracted mixed accepts a value - #6167

Open
zonuexe wants to merge 2 commits into
phpstan:2.2.xfrom
zonuexe:non-empty-mixed-accepts
Open

Check the subtracted type when a subtracted mixed accepts a value#6167
zonuexe wants to merge 2 commits into
phpstan:2.2.xfrom
zonuexe:non-empty-mixed-accepts

Conversation

@zonuexe

@zonuexe zonuexe commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Closes phpstan/phpstan#15033

MixedType::accepts (and StrictMixedType, reached once RuleLevelHelper rewrites an explicit MixedType for checkExplicitMixed) accepted every value unconditionally, so a subtracted mixed like non-empty-mixed rejected nothing at argument or return boundaries, even though the same subtraction already powered narrowing and reachability (identical.alwaysFalse, if.alwaysTrue, etc.).

Acceptance now turns to No only on a definite hit (subtractedType->isSuperTypeOf($given)->yes()), so partial overlaps (e.g. a general string into non-empty-mixed, which may or may not be '') stay accepted — this preserves mixed's usual looseness and is why the fix isn't isSuperTypeOf(...)->toAcceptsResult(). NeverType is exempted, mirroring MixedType::isSuperTypeOf.

RuleLevelHelper::transformCommonType now carries the subtraction through when it converts an explicit MixedType into StrictMixedType, instead of discarding it — otherwise level max stayed silent even with MixedType fixed. VerbosityLevel::getRecommendedLevelByType escalates to precise() when a subtracted (Strict)MixedType is involved, so messages render the subtraction instead of a bare, uninformative mixed.

@staabm
staabm force-pushed the non-empty-mixed-accepts branch from e8428d1 to a5e98b4 Compare August 3, 2026 10:46
)
) {
return new StrictMixedType();
return new StrictMixedType($type->getSubtractedType());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should these code-sites also get the subtracted type?

grafik

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — yes, two more. RuleLevelHelper::findTypeToCheck() dropped it for a plain MixedType, and TemplateMixedType::toStrictMixedType() reset the template bound to a bare StrictMixedType (that one is also reached from transformCommonType() and the intersection branch of findTypeToCheck()). Both are observable at level 9 with checkNonStringableDynamicAccess: after if ($mixed === null) { return; }, self::{$mixed} reported ... but mixed was given while the neighbouring property.nameNotString message already said mixed~null, and the template case said T of mixed instead of T of mixed~null. Both now pass the subtraction through, with a regression test in ClassConstantRuleTest; I kept the subtraction on the template's bound rather than on TemplateStrictMixedType itself, since TemplateTypeTrait already delegates all subtraction handling to the bound.

@zonuexe
zonuexe marked this pull request as draft August 3, 2026 13:39
@zonuexe
zonuexe marked this pull request as ready for review August 3, 2026 14:18
@phpstan-bot

Copy link
Copy Markdown
Collaborator

This pull request has been marked as ready for review.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

non-empty-mixed is used for narrowing but not enforced at call/return boundaries

3 participants